﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Text;
using System.Web;
using System.Xml.Linq;
using Microsoft.Office.Server.Utilities;
using Microsoft.SharePoint;
using ZhukBlogLinqExamples.Model;
using Microsoft.SharePoint.Linq;
using ZhukBlogLinqExamples.Model.Meta;

namespace ZhukBlogLinqExamples.Repositories
{
    /// <summary>
    /// Base repository class
    /// </summary>
    /// <typeparam name="TEntity"></typeparam>
    /// <typeparam name="TContext"></typeparam>
    public abstract class BaseRepository<TEntity, TContext>
        where TEntity : ZhukDataItem, new()
        where TContext : DataContext
    {
        protected readonly string WebUrl;
        protected readonly string ListName;
        protected readonly bool ReadOnly;
        protected readonly bool IsAnonymous;

        protected BaseRepository(string listName, string webUrl, bool readOnly, bool crossSite)
        {
            ReadOnly = readOnly;
            ListName = listName;
            WebUrl = webUrl;

            var ctx = SPContext.Current;
            IsAnonymous = ctx != null && SPContext.Current.Web.CurrentUser == null;
            if (crossSite)
            {
                var backupCtx = HttpContext.Current;
                HttpContext.Current = null;
                InitializeParameters();
                HttpContext.Current = backupCtx;
            }
            else
            {
                InitializeParameters();
            }
        }

        protected BaseRepository(string listName, string webUrl, bool readOnly)
            : this(listName, webUrl, readOnly, false)
        { }

        protected BaseRepository(string listName, bool readOnly)
            : this(listName,
                SPContext.Current.Web.Url, readOnly)
        { }

        protected BaseRepository(string listName)
            : this(listName, true)
        { }

        protected BaseRepository(string listName, string webUrl)
            : this(listName, webUrl, true)
        { }

        ~BaseRepository()
        {
            //if (CurrentContext != null)
            //    CurrentContext.Dispose();
        }

        private void InitializeParameters()
        {
            if (IsAnonymous)
            {
                RunAsAdmin(() =>
                {
                    CurrentContext =
                        (TContext)Activator.CreateInstance(typeof(TContext),
                        new object[] { WebUrl });
                    CurrentContext.ObjectTrackingEnabled = !ReadOnly;
                    CurrentList = CurrentContext.GetList<TEntity>(ListName);
                });
            }
            else
            {
                CurrentContext =
                    (TContext)Activator.CreateInstance(typeof(TContext),
                    new object[] { WebUrl });
                CurrentContext.ObjectTrackingEnabled = !ReadOnly;
                CurrentList = CurrentContext.GetList<TEntity>(ListName);
            }
        }

        /// <summary>
        /// IsThrottled flag
        /// </summary>
        public bool IsThrottled
        {
            get
            {
                return MetaData.List.IsThrottled;
            }
        }

        public int MaxRowsPerSelect
        {
            get
            {
                var val = MetaData.List.ParentWeb.Site.WebApplication.MaxItemsPerThrottledOperation;
                return (int)(val > int.MaxValue
                                  ? int.MaxValue
                                  : val);
            }
        }

        private TContext CurrentContext { get; set; }

        internal EntityList<TEntity> CurrentList { get; set; }

        /// <summary>
        /// Retrieving collection of entities
        /// </summary>
        public IQueryable<TEntity> GetEntityCollection()
        {
            return GetEntityCollection(entry => true);
        }

        /// <summary>
        /// Retrieving collection of entities
        /// </summary>
        /// <param name="expression">Predicate</param>
        public IQueryable<TEntity> GetEntityCollection(
            Expression<Func<TEntity, bool>> expression)
        {
            return GetEntityCollection(expression, string.Empty, true, 0);
        }

        /// <summary>
        /// Retrieving collection of entities
        /// </summary>
        /// <param name="expression">Predicate</param>
        /// <param name="path">Folder path</param>
        public IQueryable<TEntity> GetEntityCollection(
            Expression<Func<TEntity, bool>> expression, string path)
        {
            return GetEntityCollection(expression, path, true, 0);
        }

        /// <summary>
        /// Retrieving collection of entities
        /// </summary>
        /// <param name="expression">Predicate</param>
        /// <param name="path">Folder path</param>
        /// <param name="recursive">Recoursive</param>
        public IQueryable<TEntity> GetEntityCollection(
            Expression<Func<TEntity, bool>> expression,
            string path, bool recursive)
        {
            return GetEntityCollection(expression, path, recursive, 0);
        }

        /// <summary>
        /// Retrieving collection of entities
        /// </summary>
        /// <param name="expression">Predicate</param>
        /// <param name="path">Folder path</param>
        /// <param name="recursive">Recoursive</param>
        /// <param name="maxRows">Max items</param>
        public IQueryable<TEntity> GetEntityCollection(
            Expression<Func<TEntity, bool>> expression,
            string path, bool recursive, int maxRows)
        {
            var query = CurrentList
                .ScopeToFolder(path, recursive)
                .Where(expression);
            if (maxRows > 0)
                query = query.Take(maxRows);
            return query;
        }

        /// <summary>
        /// Get entity
        /// </summary>
        /// <param name="id">Id</param>
        public TEntity GetEntity(int id)
        {
            var query = CurrentList
                .ScopeToFolder(string.Empty, true)
                .Where(entry => entry.Id == id);
            return query.FirstOrDefault();
        }

        /// <summary>
        /// Delete entity
        /// </summary>
        /// <param name="entity">entity</param>
        public void DeleteEntity(TEntity entity)
        {
            if (!entity.Id.HasValue)
                throw new ArgumentException("Entity has no identifier");
            if (entity.IsCurrentVersion == true)
            {
                DeleteEntity(entity.Id.Value);
            }
            else
            {
                if (!entity.Version.HasValue)
                    throw new ArgumentException("Entity version has no identifier");
                DeleteEntityVersion(entity.Id.Value, entity.Version.Value);
            }
        }

        private void DeleteEntity(int id)
        {
            var query = CurrentList
                .ScopeToFolder(string.Empty, true)
                .Where(entry => entry.Id == id);
            var entity = query.FirstOrDefault();
            if (entity != null)
            {
                CurrentList.DeleteOnSubmit(entity);
            }
            CurrentContext.SubmitChanges();
        }

        private void DeleteEntityVersion(int entityId, int versionId)
        {
            var item = MetaData.List.GetItemById(entityId);
            var version = item.Versions.GetVersionFromLabel(versionId.ToString());
            version.Delete();
        }

        /// <summary>
        /// Save entity
        /// </summary>
        /// <param name="entity">entity</param>
        public TEntity SaveEntity(TEntity entity)
        {
            if (!entity.Id.HasValue)
                entity.EntityState = EntityState.ToBeInserted;
            if (entity.EntityState == EntityState.Unchanged)
                return entity;
            if (!EntityExistsInContext(entity))
                CurrentList.Attach(entity);
            CurrentContext.SubmitChanges();
            return entity;
        }

        /// <summary>
        /// Check entity is attached to context
        /// </summary>
        /// <param name="entity">entity</param>
        /// <returns>true - attched, false - is not attached</returns>
        public bool EntityExistsInContext(TEntity entity)
        {
            var type = CurrentContext.GetType();
            var pi = type.GetProperty("EntityTracker", BindingFlags.NonPublic | BindingFlags.Instance);
            var val = pi.GetValue(CurrentContext, null);
            var trackerType = val.GetType();
            var eidType =
                Type.GetType("Microsoft.SharePoint.Linq.EntityId, " + typeof(DataContext).Assembly.FullName);
            var eid = Activator.CreateInstance(eidType, WebUrl, MetaData.List.Title);
            var mi = trackerType.GetMethod("TryGetId", BindingFlags.Public | BindingFlags.Instance);
            var res = mi.Invoke(val, new[] { entity, eid });
            return (bool)res;
        }

        /// <summary>
        /// Lock entity
        /// </summary>
        /// <param name="id">Id</param>
        /// <param name="lockType">Lock type</param>
        /// <param name="timeSpan">Timeout</param>
        public void LockEntity(int id, SPFile.SPLockType lockType, TimeSpan timeSpan)
        {
            var file = GetFile(id);
            if (file.ListItemAllFields.ParentList is SPDocumentLibrary)
            {
                if (file.LockType == SPFile.SPLockType.None)
                {
                    file.Lock(lockType, Guid.NewGuid().ToString("N"), timeSpan);
                }
            }
        }

        /// <summary>
        /// Release lock
        /// </summary>
        /// <param name="id">Id</param>
        public void ReleaseLockEntity(int id)
        {
            var file = GetFile(id);
            if (file.LockType != SPFile.SPLockType.None)
            {
                file.ReleaseLock(file.LockId);
            }
        }

        /// <summary>
        /// Refresh lock (prologation)
        /// </summary>
        /// <param name="id">Id</param>
        /// <param name="timeSpan">Timeout</param>
        public void RefreshLockEntity(int id, TimeSpan timeSpan)
        {
            var file = GetFile(id);
            if (file.LockType != SPFile.SPLockType.None)
            {
                file.RefreshLock(file.LockId, timeSpan);
            }
        }

        /// <summary>
        /// Get file representing the entity
        /// </summary>
        /// <param name="id">Id</param>
        /// <returns>SPFile</returns>
        protected SPFile GetFile(int id)
        {
            var entity = GetEntity(id);
            var list = MetaData.List;
            var web = list.ParentWeb;
            var file = web.GetFile(entity.ServerUrl);
            return file;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="id">Id</param>
        public EntityLockInfo GetLockInfo(int id)
        {
            var file = GetFile(id);
            var info = new EntityLockInfo(file);
            return info;
        }

        /// <summary>
        /// EntityList meta data
        /// </summary>
        public EntityListMetaData MetaData
        {
            get
            {
                return EntityListMetaData.GetMetaData(CurrentList);
            }
        }

        public void RunAsAdmin(SPSecurity.CodeToRunElevated secureCode)
        {
            var nullUserFlag = (SPContext.Current != null && IsAnonymous);
            if (nullUserFlag)
            {
                var backupCtx = HttpContext.Current;
                HttpContext.Current = null;
                SPSecurity.RunWithElevatedPrivileges(secureCode);
                HttpContext.Current = backupCtx;
            }
            else
            {
                SPSecurity.RunWithElevatedPrivileges(secureCode);
            }
        }

        /// <summary>
        /// Retrieve versions of entity
        /// </summary>
        /// <param name="entity">Entity</param>
        /// <returns>Collection of entities</returns>
        public IEnumerable<TEntity> GetVersions(TEntity entity)
        {
            if (entity.Id.HasValue)
                return GetVersions(entity.Id.Value);
            throw new ArgumentException("Entity has no id property value");
        }

        private IEnumerable<TEntity> GetVersions(int id)
        {
            var item = MetaData.List.GetItemById(id);
            var versions = item.Versions
                .Cast<SPListItemVersion>()
                .Select(v => Activator.CreateInstance(typeof(TEntity), v))
                .Cast<TEntity>()
                .ToList();
            return versions;
        }

        #region Truncate List

        /// <summary>
        /// Truncate current list
        /// </summary>
        public void TruncateList()
        {
            var coresQnt = Environment.ProcessorCount;
            var items = CurrentList.Select(x => x.Id.Value).ToList();
            var web = MetaData.List.ParentWeb;
            web.AllowUnsafeUpdates = true;
            var list = MetaData.List;
            for (var i = 0; i < coresQnt; i++)
            {
                var evenWork = new ProcessBatchDataDelegate(ProcessBatchDataHandler);
                evenWork.BeginInvoke(items, list.ID, coresQnt, i, web, ProcessBatchDataCallback, evenWork);
            }
        }

        private delegate void ProcessBatchDataDelegate(IList<int> items, Guid listId, int cpuCoresQnt, int coreIndex, SPWeb web);

        private static void ProcessBatchDataCallback(IAsyncResult result)
        {
            var flusher = (ProcessBatchDataDelegate)result.AsyncState;
            flusher.EndInvoke(result);
        }

        private static void ProcessBatchDataHandler(IList<int> items, Guid listId, int cpuCoresQnt, int coreIndex, SPWeb web)
        {
            var methods = new StringBuilder(10);
            for (var i = coreIndex; i < items.Count(); i = i + cpuCoresQnt)
            {
                var item = items[i];
                var cmd = GetBatchDeleteCommand(listId, item);
                methods.Append(cmd);
            }
            if (methods.Length <= 0) return;
            var batch =
                string.Format(
                    @"<?xml version=""1.0"" encoding=""UTF-8""?><ows:Batch OnError=""Continue"">{0}</ows:Batch>",
                    methods.ToString());
            web.ProcessBatchData(batch);
        }

        private static string GetBatchDeleteCommand(Guid listId, int itemId)
        {
            var sb = new StringBuilder(10);
            sb.AppendFormat(@"<Method ID=""{0}, Delete"">{1}",
                Guid.NewGuid(),
                Environment.NewLine);
            sb.AppendFormat(@"<SetList>{0}</SetList>{1}",
                listId,
                Environment.NewLine);
            sb.AppendFormat(@"<SetVar Name=""ID"">{0}</SetVar>{1}",
                itemId,
                Environment.NewLine);
            sb.AppendLine(@"<SetVar Name=""Cmd"">Delete</SetVar>");
            sb.AppendLine(@"</Method>");
            return sb.ToString();
        }

        #endregion
    }
}
